/*
 * Copyright 2008 to the author or authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.lucifer.scene.importers.ase;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.Reader;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.StringTokenizer;

import org.lucifer.scene.importers.SceneImportException;
import org.lucifer.scene.importers.SceneImporter;
import org.lucifer.scene.importers.UnrecognizedSceneTypeException;
import org.lucifer.scene.importers.util.RawCameraRepresentation;
import org.lucifer.scene.importers.util.RawMaterialRepresentation;
import org.lucifer.scene.importers.util.RawMeshRepresentation;
import org.lucifer.scene.importers.util.RawSceneRepresentation;

/**
 * Imports from the ASCII Scene Export (ASE) format to the Lucifer (LCF) format.
 * 
 * @author Mario Gonzalez
 * @author Henrik Engstroem
 */
public class ASESceneImporter implements SceneImporter {
	// Lets us know what state we're in
	private enum State {
		IDLE, MATERIAL, GEOMETRY, CAMERA
	};

	// Constants for different data elements in the ASE file format
	private static final String cNODE_NAME = "*NODE_NAME";
	private static final String cMESH_VERTEX = "*MESH_VERTEX";
	private static final String cMESH_FACE = "*MESH_FACE";
	private static final String cMESH_NORMALS = "*MESH_NORMALS";
	private static final String cGEOMOBJECT = "*GEOMOBJECT";
	private static final String cMATERIAL_LIST = "*MATERIAL_LIST";
	private static final String cMATERIAL_COUNT = "*MATERIAL_COUNT";
	private static final String cMATERIAL = "*MATERIAL";
	private static final String cMATERIAL_NAME = "*MATERIAL_NAME";
	private static final String cMATERIAL_DIFFUSE = "*MATERIAL_DIFFUSE";
	private static final String cMATERIAL_REF = "*MATERIAL_REF";
	private static final String cMESH_VERTEXNORMAL = "*MESH_VERTEXNORMAL";
	private static final String cMESH_FACENORMAL = "*MESH_FACENORMAL";
	private static final String cMESH_NUMVERTEX = "*MESH_NUMVERTEX";
	private static final String cMESH_NUMFACES = "*MESH_NUMFACES";
	private static final String cTM_ROW0 = "*TM_ROW0";
	private static final String cTM_ROW1 = "*TM_ROW1";
	private static final String cTM_ROW2 = "*TM_ROW2";
	private static final String cCAMERAOBJECT = "*CAMERAOBJECT";
	private static final String cTM_POS = "*TM_POS";

	// Constant for the charset used to read the file
	private static final String cCHARSET = "ISO-8859-1";

	// Default file for parsing
	private static final String cPARSE_FILE = "K:/XP/My Documents/3dsmax/export/main_test_scene.ase";

	// The raw scene representation we're reading all the data into
	private RawSceneRepresentation _sceneRepresentation;

	// Just a pointer to the current material instance being worked with
	private RawMaterialRepresentation _currentMaterial;

	// A pointer to the current geometry mesh being populated
	private RawMeshRepresentation _currentMesh;

	// The state
	private State _state;

	/**
	 * Class constructor
	 */
	public ASESceneImporter() {
		// Set a neutral, initial state
		_state = State.IDLE;
	}

	/**
	 * Imports a file of the ASE file format to the LCF file format.
	 * 
	 * @param aFromFileName
	 *            the complete file name and path to the file to import
	 * @param aToFileName
	 *            the complete file name and path to the resulting file
	 */
	public void importScene(String aFromFileName, String aToFileName)
			throws UnrecognizedSceneTypeException, SceneImportException {
	}

	/**
	 * Parses the contents of the file into a temporary memory storage.
	 * 
	 * @param aFileName
	 *            the name (and path) of the file to parse.
	 */
	private void parseFile(String aFileName) {
		try {
			// Open the file and read it
			FileInputStream in = new FileInputStream(aFileName);
			FileChannel channel = in.getChannel();
			Reader infile = Channels.newReader((ReadableByteChannel) channel,
					cCHARSET);
			BufferedReader reader = new BufferedReader(infile);
			String currLine;

			// Initialize the scene representation
			_sceneRepresentation = new RawSceneRepresentation();

			// State variables for the inner loop
			boolean ignoreMaterial = false;
			int faceCounter = 0;
			int currentFaceIndex = 0;
			int normalComponentNum = 0;
			int numFaces = 0;

			// Matrix rows
			double[][] transformationMatrix = new double[3][3];

			// Iterate through all lines
			while ((currLine = reader.readLine()) != null) {
				// Tokenize the string and remove unnecessary white space
				StringTokenizer tokenizer = new StringTokenizer(currLine);

				// Represents a tokenized line
				String[] line = new String[tokenizer.countTokens()];

				// Local index counter
				int tokensRead = 0;

				// Split the string manually
				while (tokenizer.hasMoreTokens()) {
					// Place the current token in the array
					line[tokensRead] = tokenizer.nextToken();
					tokensRead++;
				}

				// Now, parse the contents
				if (line[0].equals(cMATERIAL_LIST)) {
					// Set the proper state
					_state = State.MATERIAL;
				} else if (line[0].equals(cMATERIAL_COUNT)) {
					// The number of materials in this scene
					_sceneRepresentation._materials = new RawMaterialRepresentation[Integer
							.parseInt(line[1])];
				} else if (line[0].equals(cMATERIAL)) {
					// Create the current material;
					_currentMaterial = new RawMaterialRepresentation();

					// Set the id of the current material
					_currentMaterial._id = Integer.parseInt(line[1]);

					// Add the material to the scene representation
					_sceneRepresentation._materials[_currentMaterial._id] = _currentMaterial;

					// It's a proper material and not a submaterial
					ignoreMaterial = false;
				} else if (line[0].equals(cMATERIAL_NAME) && !ignoreMaterial) {
					// Read out the material name
					StringBuffer nameBuffer = new StringBuffer(line.length);

					// If the name string had spaces, the tokenizer most
					// likely split the name into smaller strings
					for (int i = 1; i < line.length; i++) {
						nameBuffer.append(line[i]);
					}

					// Set the name of the current material (remove leading
					// and trailing quotation marks.
					_currentMaterial._name = nameBuffer.substring(1, nameBuffer
							.length() - 1);
				} else if (line[0].equals(cMATERIAL_DIFFUSE) && !ignoreMaterial) {
					// Set & get the color components of the material
					_currentMaterial._red = Double.parseDouble(line[1]);
					_currentMaterial._green = Double.parseDouble(line[2]);
					_currentMaterial._blue = Double.parseDouble(line[3]);

					// Nothing more to read pertaining to this material,
					// make sure that sub materials aren't interpreted
					// as proper materials.
					ignoreMaterial = true;
				} else if (line[0].equals(cGEOMOBJECT)) {
					// Switch state
					_state = State.GEOMETRY;

					if (_currentMesh != null) {
						_sceneRepresentation._meshList.add(_currentMesh);
					}

					// Allocate a new mesh
					_currentMesh = new RawMeshRepresentation();
				} else if (line[0].equals(cNODE_NAME)) {
					if ((_state == State.GEOMETRY)
							&& _currentMesh._name == null) {
						// Read out the material name
						StringBuffer nameBuffer = new StringBuffer();

						// If the name string had spaces, the tokenizer most
						// likely split the name into smaller strings
						for (int i = 1; i < line.length; i++) {
							nameBuffer.append(line[i]);
						}

						_currentMesh._name = nameBuffer.substring(1, nameBuffer
								.length() - 1);
					}
				} else if (line[0].equals(cTM_ROW0)) {
					handleTransformationMatrix(transformationMatrix, line, 0);
				} else if (line[0].equals(cTM_ROW1)) {
					handleTransformationMatrix(transformationMatrix, line, 1);
				} else if (line[0].equals(cTM_ROW2)) {
					handleTransformationMatrix(transformationMatrix, line, 2);
				} else if (line[0].equals(cMESH_NUMVERTEX)) {
					_currentMesh._vertices = new double[Integer
							.parseInt(line[1])][3];
				} else if (line[0].equals(cMESH_NUMFACES)) {
					numFaces = Integer.parseInt(line[1]);
					_currentMesh._faces = new int[numFaces][3];
					faceCounter = 0;
				} else if (line[0].equals(cMESH_VERTEX)) {
					int vertexIndex = Integer.parseInt(line[1]);
					_currentMesh._vertices[vertexIndex][0] = Double
							.parseDouble(line[2]);
					_currentMesh._vertices[vertexIndex][1] = Double
							.parseDouble(line[4]);
					_currentMesh._vertices[vertexIndex][2] = -(Double
							.parseDouble(line[3]));
				} else if (line[0].equals(cMESH_FACE)) {
					_currentMesh._faces[faceCounter][0] = Integer
							.parseInt(line[3]);
					_currentMesh._faces[faceCounter][1] = Integer
							.parseInt(line[5]);
					_currentMesh._faces[faceCounter][2] = Integer
							.parseInt(line[7]);
					faceCounter++;
				} else if (line[0].equals(cMESH_NORMALS)) {
					_currentMesh._vertexNormals = new double[numFaces][3][3];
				} else if (line[0].equals(cMESH_FACENORMAL)) {
					currentFaceIndex = Integer.parseInt(line[1]);
				} else if (line[0].equals(cMESH_VERTEXNORMAL)) {
					handleVertexNormal(currentFaceIndex, normalComponentNum,
							transformationMatrix, line);

					// Increase the component number counter
					normalComponentNum = (normalComponentNum == 3 ? 0
							: normalComponentNum++);
				} else if (line[0].equals(cMATERIAL_REF)) {
					// Set the material reference
					_currentMesh._materialRef = Integer.parseInt(line[1]);
				} else if (line[0].equals(cCAMERAOBJECT)) {
					// Now reading camera settings
					_state = State.CAMERA;
					
					// Initialize the camera of the scene
					_sceneRepresentation._camera = 
						new RawCameraRepresentation();
				} else if(line[0].equals(cTM_POS)) {
					// We're only interested in positions if we're reading
					// camera settings 
					if(_state == State.CAMERA) {
						if(_sceneRepresentation._camera._position == null) {
							// Create the position
							_sceneRepresentation._camera._position = 
								new double[3];
							
							// Read the coordinates
							_sceneRepresentation._camera._position[0] = 
								Double.parseDouble(line[1]);
							_sceneRepresentation._camera._position[2] = 
								-Double.parseDouble(line[2]);
							_sceneRepresentation._camera._position[1] = 
								Double.parseDouble(line[3]);
						} else if(_sceneRepresentation._camera._target == null) {
							// Create the target
							_sceneRepresentation._camera._target = 
								new double[3];
							
							// Read the coordinates
							_sceneRepresentation._camera._target[0] = 
								Double.parseDouble(line[1]);
							_sceneRepresentation._camera._target[2] = 
								-Double.parseDouble(line[2]);
							_sceneRepresentation._camera._target[1] = 
								Double.parseDouble(line[3]);
						}
					}					
				}
			}

			// Add that last mesh
			if (_currentMesh != null) {
				_sceneRepresentation._meshList.add(_currentMesh);
			}

			// Close the reader
			reader.close();
		} catch (Exception e) {
			// TODO: Better exception handling / logging
			e.printStackTrace();
		}
	}

	/*
	 * Convenience method
	 */
	private void handleVertexNormal(int aCurrentFaceIndex,
			int aNormalComponentNum, double[][] aTransformationMatrix,
			String[] aLine) {
		// Get the components
		double x = Double.parseDouble(aLine[2]);
		double y = Double.parseDouble(aLine[3]);
		double z = Double.parseDouble(aLine[4]);

		// Transform the components
		double tmpx = (aTransformationMatrix[0][0] * x
				+ aTransformationMatrix[1][0] * y + aTransformationMatrix[2][0]
				* z);
		double tmpz = -(aTransformationMatrix[0][1] * x
				+ aTransformationMatrix[1][1] * y + aTransformationMatrix[2][1]
				* z);
		double tmpy = (aTransformationMatrix[0][2] * x
				+ aTransformationMatrix[1][2] * y + aTransformationMatrix[2][2]
				* z);

		// Set the values
		_currentMesh._vertexNormals[aCurrentFaceIndex][aNormalComponentNum][0] = tmpx;
		_currentMesh._vertexNormals[aCurrentFaceIndex][aNormalComponentNum][1] = tmpy;
		_currentMesh._vertexNormals[aCurrentFaceIndex][aNormalComponentNum][2] = tmpz;
	}

	/*
	 * Convenience method
	 */
	private void handleTransformationMatrix(double[][] aTransformationMatrix,
			String[] aLine, int aIndex) {
		aTransformationMatrix[aIndex][0] = Double.parseDouble(aLine[1]);
		aTransformationMatrix[aIndex][1] = Double.parseDouble(aLine[2]);
		aTransformationMatrix[aIndex][2] = Double.parseDouble(aLine[3]);
	}

	/**
	 * The first argument to this main method should be the file you want to
	 * use. If no file is sent then a default file will be used.
	 * 
	 * @param aArgs
	 */
	public static void main(String[] aArgs) {
		ASESceneImporter importer = new ASESceneImporter();
		long memBefore = Runtime.getRuntime().totalMemory()
				- Runtime.getRuntime().freeMemory();
		long startTime = System.currentTimeMillis();
		if (aArgs != null && aArgs[0] != null) {
			importer.parseFile(aArgs[0]);
		} else {
			importer.parseFile(cPARSE_FILE);
		}
		
		System.out.println("Time taken: "
				+ (System.currentTimeMillis() - startTime));
		long memAfter = Runtime.getRuntime().totalMemory()
				- Runtime.getRuntime().freeMemory();
		System.out.println("Mem growth: " + (memAfter - memBefore));
	}
}
